//-------------------------------------------------------------------------------------
// <copyright file="Create.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation. All rights reserved.
//      THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
//      EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
//      WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// </copyright>
// <author>Greg Beech</author>
// <email>gregbee@microsoft.com</email>
// <date>2004-04-20</date>
// <summary>
//      Creates a share on either the local or a remote machine.
// </summary>
//-------------------------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.Folder.Share
{
    #region Using directives

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Management;
    using System.Text;

    using Microsoft.Build.Framework;
    using Microsoft.Build.Utilities;

    #endregion

    #region Class Comments
    /// <summary>
    /// Creates a share on either the local or a remote machine.
    /// </summary>
    /// <remarks>
    /// <code><![CDATA[<Share.Create ShareName="shareName" Path="path" Type="type" Description="description" AllowedUsers="allowedUsers" DeniedUsers="deniedUsers" MaximumAllowed="maximumAllowed" Machine="machine" UserName="userName" Password="password" />]]></code>
    /// <para>where:</para>
    /// <para><i>shareName (Required)</i></para>
    /// <para>
    /// The name of the share to create.
    /// </para>
    /// <para><i>path</i></para>
    /// <para>
    /// The full physical path to create the share at.
    /// </para>
    /// <para><i>type</i></para>
    /// <para>
    /// The type of share to create. This can be one of the following values: DiskDrive, PrintQueue, Device,
    /// Ipc, DiskDriveAdmin, PrintQueueAdmin, DeviceAdmin, IpcAdmin. The default is DiskDrive.
    /// </para>
    /// <para><i>description</i></para>
    /// <para>
    /// A description of the share that users can see when they connect to it.
    /// </para>
    /// <para><i>allowedUsers</i></para>
    /// <para>
    /// A delimited list of the users or security groups who are allowed to access the share, in the format
    /// DOMAIN\Username. If none are supplied and nobody is in the <i>deniedUsers</i> then everybody is
    /// given full access to the share. If none are supplied there are accounts in <i>deniedUsers</i>
    /// then nobody is given access to the share.
    /// </para>
    /// <para><i>deniedUsers</i></para>
    /// <para>
    /// A delimited list of the users or security groups who are explicitly denied to access the share, in the 
    /// format DOMAIN\Username. If none are supplied then nobody is explicitly denied access to the share.
    /// </para>
    /// <para><i>maximumAllowed</i></para>
    /// <para>
    /// The maximum number of users who are allowed to simultaneously connect to the share. Use zero for
    /// no limit. The default is no limit.
    /// </para>
    /// <para><i>machine</i></para>
    /// <para>
    /// The name or IP address of the machine to create the share on. The default is the local machine.
    /// </para>
    /// <para><i>userName</i></para>
    /// <para>
    /// If different credentials are needed to connect to a remote machine, this is the user name in the form 
    /// DOMAIN\Username. This must not be supplied for the local machine. The default is to use the same
    /// credentials as the user the task is running as.
    /// </para>
    /// <para><i>password</i></para>
    /// <para>
    /// The password for the user account specified by <i>userName</i>. The default is no password.
    /// </para>
    /// <seealso cref="Microsoft.Sdc.Tasks.Folder.Share.DeleteShare"/>
    /// <seealso cref="Microsoft.Sdc.Tasks.Folder.Share.Connect"/>
    /// <seealso cref="Microsoft.Sdc.Tasks.Folder.Share.Disconnect"/>
    /// <seealso cref="Microsoft.Sdc.Tasks.Folder.Share.Exists"/>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// <Project>
    ///     <Target Name="Test">
    ///         <Share.Create
    ///             ShareName="Software" 
    ///             Path="C:\Software\" 
    ///             Type="DiskDrive" 
    ///             Description="Software library" 
    ///             AllowedUsers="domain\username;domain\domain admins;MyMachine\Administrator"
    ///             DeniedUsers="domain\guests;MyMachine\Guest"
    ///             MaximumAllowed="10" 
    ///             Machine="MyMachine" 
    ///             UserName="domain\username" 
    ///             Password="password1" />
    ///     </Target>
    /// </Project>
    /// ]]></code>    
    /// </example>
    #endregion
    public class Create : ShareTaskBase
    {
        #region Member Variables

        private string description;
        private int maximumAllowed = 0;
        private string path;
        private ShareType type = ShareType.DiskDrive;
        private string[] allowedUsers;
        private string[] deniedUsers;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new task to create a share.
        /// </summary>
        public Create()
        {
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the names of the users or groups that are allowed to access the share. If no users are
        /// specified then everyone is given full control. The names should be in the format DOMAIN\Username.
        /// </summary>
        /// <value>
        /// The names of the users that are allowed to access the share.
        /// </value>
        public string[] AllowedUsers
        {
            get
            {
                return (this.allowedUsers == null) ? new string[0] : this.allowedUsers;
            }

            set
            {
                this.allowedUsers = value;
            }
        }

        /// <summary>
        /// Gets or sets the names of the users or groups that are denied to access the share. If no users are
        /// specified then nobody is denied. The names should be in the format DOMAIN\Username.
        /// </summary>
        /// <value>
        /// The names of the users that are allowed to access the share.
        /// </value>
        public string[] DeniedUsers
        {
            get
            {
                return (this.deniedUsers == null) ? new string[0] : this.deniedUsers;
            }

            set
            {
                this.deniedUsers = value;
            }
        }

        /// <summary>
        /// Gets or sets the description of the share that is displayed to users.
        /// </summary>
        /// <value>
        /// The description of the share that is displayed to users.
        /// </value>
        public string Description
        {
            get
            {
                return (this.description == null) ? string.Empty : this.description;
            }

            set
            {
                this.description = value;
            }
        }

        /// <summary>
        /// Gets or sets the maximum number of users that are allowed to use the share. A value of zero
        /// indicates that there is no upper limit.
        /// </summary>
        /// <value>
        /// The maximum number of users that are allowed to use the share.
        /// </value>
        public int MaximumAllowed
        {
            get
            {
                return this.maximumAllowed;
            }

            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException("value", value, "The maximum number of users must be greater than or equal to zero.");
                }
                this.maximumAllowed = value;
            }
        }

        /// <summary>
        /// Gets or sets the physical path that the share is created at.
        /// </summary>
        /// <value>
        /// The physical path that the share is created at.
        /// </value>
        [Required]
        public string Path
        {
            get
            {
                return (this.path == null) ? string.Empty : this.path;
            }

            set
            {
                this.path = System.IO.Path.GetFullPath(value);
            }
        }

        /// <summary>
        /// Gets or sets the type of share to create. The default is "DiskDrive".
        /// </summary>
        /// <value>
        /// The type of share to create.
        /// </value>
        [CLSCompliant(false)]
        public string Type
        {
            get
            {
                return this.type.ToString();
            }

            set
            {
                this.type = (ShareType)Enum.Parse(typeof(ShareType), value);
            }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Executes the task
        /// </summary>
        protected override void InternalExecute()
        {
            //connect to the machine
            ManagementScope scope = this.GetManagementScope();
            ManagementPath sharePath = new ManagementPath("Win32_Share");
            ManagementClass shareClass = new ManagementClass(scope, sharePath, null);
            
            //create input params
            ManagementBaseObject inputParams = shareClass.GetMethodParameters("Create");
            inputParams["Path"] = this.path;
            inputParams["Name"] = this.ShareName;
            inputParams["Type"] = (uint)this.type;
            if (this.maximumAllowed > 0)
            {
                inputParams["MaximumAllowed"] = this.maximumAllowed;
            }
            inputParams["Description"] = this.description;
            inputParams["Access"] = this.BuildSecurityDescriptor(scope);
            
            //execute the method and check the return code
            ManagementBaseObject outputParams = shareClass.InvokeMethod("Create", inputParams, null);
            ShareReturnCode returnCode = (ShareReturnCode)Convert.ToUInt32(outputParams.Properties["ReturnValue"].Value, CultureInfo.InvariantCulture);
            switch (returnCode)
            {
                case ShareReturnCode.Success:
                    break;
                case ShareReturnCode.ShareAlreadyExists:
                    this.Log.LogWarning("Share already exists so will not be recreated.");
                    break;
                default:
                    throw new ApplicationException(string.Format(
                    CultureInfo.InvariantCulture,
                    "Failed to create the share: {0} (code {1}).",
                    returnCode,
                    (uint)returnCode));
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Builds a Win32_SecurityDescriptor <see cref="System.Management.ManagementObject"/> representing
        /// the users allowed and denied access to the share.
        /// </summary>
        /// <param name="scope">
        /// The <see cref="System.Management.ManagementScope"/> to build the security descriptor in.
        /// </param>
        /// <returns>
        /// A Win32_SecurityDescriptor <see cref="System.Management.ManagementObject"/>.
        /// </returns>
        private ManagementObject BuildSecurityDescriptor(ManagementScope scope)
        {
            //check inputs
            if (scope == null)
            {
                throw new ArgumentNullException("scope");
            }
            
            //build the security descriptor
            ManagementPath securityDescriptorPath = new ManagementPath("Win32_SecurityDescriptor");
            ManagementObject securityDescriptor = new ManagementClass(scope, securityDescriptorPath, null).CreateInstance();
            securityDescriptor.Properties["ControlFlags"].Value = 0x1U | 0x2U | 0x4U | 0x20U;    //default owner | default group | DACL present | default SACL
            securityDescriptor.Properties["DACL"].Value = this.BuildAccessControlList(scope);
            return securityDescriptor;
        }

        /// <summary>
        /// Builds an array of Win32_ACE <see cref="System.Management.ManagementObject"/>s that can be
        /// used as an access control list for the users specified in this class.
        /// </summary>
        /// <param name="scope">
        /// The <see cref="System.Management.ManagementScope"/> get the access control entries from.
        /// </param>
        /// <returns>
        /// An array of Win32_ACE <see cref="System.Management.ManagementObject"/>s.
        /// </returns>
        private ManagementObject[] BuildAccessControlList(ManagementScope scope)
        {
            //check inputs
            if (scope == null)
            {
                throw new ArgumentNullException("scope");
            }
            
            //build the acl - may as well do this just by adding to a list and converting to an array
            //as the WMI stuff is so slow that a bit of inefficiency here won't make much difference
            List<ManagementObject> acl = new List<ManagementObject>();
            if (this.allowedUsers != null && this.allowedUsers.Length > 0)
            {
                for (int i = 0; i < this.allowedUsers.Length; i++)
                {
                    ManagementObject trustee = this.BuildTrustee(scope, this.allowedUsers[i]);
                    acl.Add(this.BuildAccessControlEntry(scope, trustee, false));
                }
            }

            if (this.deniedUsers != null && this.deniedUsers.Length > 0)
            {
                for (int i = 0; i < this.deniedUsers.Length; i++)
                {
                    ManagementObject trustee = this.BuildTrustee(scope, this.deniedUsers[i]);
                    acl.Add(this.BuildAccessControlEntry(scope, trustee, true));
                }
            }
            return acl.ToArray();
        }

        /// <summary>
        /// Builds a Win32_ACE <see cref="System.Management.ManagementObject"/> for a trustee.
        /// </summary>
        /// <remarks>
        /// To simplify this class and the inputs, this ACE just uses full permissions for the trustee, so
        /// an "allow" ACE will grant all permissions and a "deny" ACE will deny all permissions. This should
        /// be fine as this is only access to the share, and the actual access to the files will be controlled
        /// by the computer's file system permissions which we don;t change.
        /// </remarks>
        /// <param name="scope">
        /// The <see cref="System.Management.ManagementScope"/> to create the trustee in.
        /// </param>
        /// <param name="trustee">
        /// The Win32_Trustee <see cref="System.Management.ManagementObject"/> to create the ACE for.
        /// </param>
        /// <param name="deny">
        /// If <b>true</b> then this is a deny ACE, else it is an allow one.
        /// </param>
        /// <returns>
        /// A Win32_ACE <see cref="System.Management.ManagementObject"/>.
        /// </returns>
        private ManagementObject BuildAccessControlEntry(ManagementScope scope, ManagementObject trustee, bool deny)
        {
            //check inputs
            if (scope == null)
            {
                throw new ArgumentNullException("scope");
            }

            if (trustee == null)
            {
                throw new ArgumentNullException("trustee");
            }

            //build the ace
            ManagementPath acePath = new ManagementPath("Win32_ACE");
            ManagementObject ace = new ManagementClass(scope, acePath, null).CreateInstance();
            ace.Properties["AccessMask"].Value = 0x1U | 0x2U | 0x4U | 0x8U | 0x10U | 0x20U | 0x40U | 0x80U | 0x100U | 0x10000U | 0x20000U | 0x40000U | 0x80000U | 0x100000U;    //all permissions
            ace.Properties["AceFlags"].Value = 0x0U;                //no flags
            ace.Properties["AceType"].Value = (deny) ? 1U : 0U;     //0 = allow, 1 = deny
            ace.Properties["Trustee"].Value = trustee;
            return ace;
        }

        /// <summary>
        /// Builds a Win32_Trustee <see cref="System.Management.ManagementObject"/> for a user account.
        /// </summary>
        /// <param name="scope">
        /// The <see cref="System.Management.ManagementScope"/> to create the trustee in.
        /// </param>
        /// <param name="username">
        /// The username for the trustee, in the form DOMAIN\Username.
        /// </param>
        /// <returns>
        /// A Win32_Trustee <see cref="System.Management.ManagementObject"/>.
        /// </returns>
        private ManagementObject BuildTrustee(ManagementScope scope, string username)
        {
            //check inputs
            if (scope == null)
            {
                throw new ArgumentNullException("scope");
            }
            
            if (string.IsNullOrEmpty(username))
            {
                throw new ArgumentNullException("username");
            }
            
            if (!username.Contains(@"\"))
            {
                throw new FormatException(@"The username must be in the format DOMAIN\Username.");
            }
            
            //build the trustee
            string[] usernameParts = username.Split('\\');
            string domain = usernameParts[0];
            string alias = usernameParts[1];
            ManagementObject account = this.GetAccount(scope, domain, alias);
            ManagementObject sid = this.GetSecurityIdentifier(scope, account);
            ManagementPath trusteePath = new ManagementPath("Win32_Trustee");
            ManagementObject trustee = new ManagementClass(scope, trusteePath, null).CreateInstance();
            trustee.Properties["Domain"].Value = domain;
            trustee.Properties["Name"].Value = alias;
            trustee.Properties["SID"].Value = sid.Properties["BinaryRepresentation"].Value;
            trustee.Properties["SidLength"].Value = sid.Properties["SidLength"].Value;
            trustee.Properties["SIDString"].Value = sid.Properties["SID"].Value;
            return trustee;
        }

        /// <summary>
        /// Gets a Win32_Account <see cref="System.Management.ManagementObject"/> for a user.
        /// </summary>
        /// <param name="scope">
        /// The <see cref="System.Management.ManagementScope"/> to get the account from.
        /// </param>
        /// <param name="domain">
        /// The domain of the account.
        /// </param>
        /// <param name="alias">
        /// The alias of the account.
        /// </param>
        /// <returns>
        /// A Win32_Account <see cref="System.Management.ManagementObject"/>.
        /// </returns>
        private ManagementObject GetAccount(ManagementScope scope, string domain, string alias)
        {
            //check inputs
            if (scope == null)
            {
                throw new ArgumentNullException("scope");
            }

            if (string.IsNullOrEmpty("domain"))
            {
                throw new ArgumentNullException("domain");
            }

            if (string.IsNullOrEmpty("alias"))
            {
                throw new ArgumentNullException("alias");
            }
            
            //get the account - try to get it by searching those on the machine first which gets local accounts
            string queryString = string.Format(CultureInfo.InvariantCulture, "select * from Win32_Account where Name = '{0}' and Domain='{1}'", alias, domain);
            ObjectQuery query = new ObjectQuery(queryString);
            ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
            foreach (ManagementObject returnedAccount in searcher.Get())
            {
                return returnedAccount;
            }
            
            //didn't find it on the machine so we'll try to bind to it using a path - this works for domaina accounts
            string accountPathString = string.Format(CultureInfo.InvariantCulture, "Win32_Account.Name='{0}',Domain='{1}'", alias, domain);
            ManagementPath accountPath = new ManagementPath(accountPathString);
            ManagementObject account = new ManagementObject(accountPath);
            try
            {
                account.Get();
                return account;
            }
            catch (ManagementException ex)
            {
                throw new ApplicationException(string.Format(CultureInfo.InvariantCulture, @"Could not find account '{0}\{1}'.", domain, alias), ex);
            }
        }

        /// <summary>
        /// Gets a Win32_SID <see cref="System.Management.ManagementObject"/>.
        /// </summary>
        /// <param name="scope">
        /// The <see cref="System.Management.ManagementScope"/> to get the SID from.
        /// </param>
        /// <param name="account">
        /// A Win32_Account <see cref="System.Management.ManagementObject"/> to get the SID for.
        /// </param>
        /// <returns>
        /// A Win32_SID <see cref="System.Management.ManagementObject"/>.
        /// </returns>
        private ManagementObject GetSecurityIdentifier(ManagementScope scope, ManagementObject account)
        {
            //check inputs
            if (scope == null)
            {
                throw new ArgumentNullException("scope");
            }

            if (account == null)
            {
                throw new ArgumentNullException("account");
            }

            //get the sid
            string sidString = (string)account.Properties["SID"].Value;
            string sidPathString = string.Format(CultureInfo.InvariantCulture, "Win32_SID.SID='{0}'", sidString);
            ManagementPath sidPath = new ManagementPath(sidPathString);
            ManagementObject sid = new ManagementObject(sidPath);
            try
            {
                sid.Get();
                return sid;
            }
            catch (ManagementException ex)
            {
                throw new ApplicationException(string.Format(CultureInfo.InvariantCulture, @"Could not find SID '{0}' for account '{1}\{2}'.", sidString, account.Properties["Domain"].Value, account.Properties["Name"].Value), ex);
            }
        }

        #endregion
    }
}